/*
 * Copyright 2010 Alibaba Group Holding Limited.
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * @(#)Clean.java   1.11 2000/08/16
 *
 */

package org.w3c.tidy;

/**
 * Clean up misuse of presentation markup (c) 1998-2000 (W3C) MIT, INRIA, Keio
 * University See Tidy.java for the copyright notice. Derived from <a
 * href="http://www.w3.org/People/Raggett/tidy"> HTML Tidy Release 4 Aug
 * 2000</a>
 *
 * @author Dave Raggett <dsr@w3.org>
 * @author Andy Quick <ac.quick@sympatico.ca> (translation to Java)
 * @version 1.11, 2000/08/16 Tidy Release 4 Aug 2000
 */

/*
 * Filters from other formats such as Microsoft Word often make excessive use of
 * presentation markup such as font tags, B, I, and the align attribute. By
 * applying a set of production rules, it is straight forward to transform this
 * to use CSS. Some rules replace some of the children of an element by style
 * properties on the element, e.g. <p><b>...</b></p> -> <p
 * style="font-weight: bold">...</p> Such rules are applied to the element's
 * content and then to the element itself until none of the rules more apply.
 * Having applied all the rules to an element, it will have a style attribute
 * with one or more properties. Other rules strip the element they apply to,
 * replacing it by style properties on the contents, e.g.
 * <dir><li><p>...</li></dir> -> <p style="margin-left 1em">... These rules are
 * applied to an element before processing its content and replace the current
 * element by the first element in the exposed content. After applying both sets
 * of rules, you can replace the style attribute by a class value and style rule
 * in the document head. To support this, an association of styles and class
 * names is built. A naive approach is to rely on string matching to test when
 * two property lists are the same. A better approach would be to first sort the
 * properties before matching.
 */

public class Clean {

    private int classNum = 1;

    private TagTable tt;

    public Clean(TagTable tt) {
        this.tt = tt;
    }

    private StyleProp insertProperty(StyleProp props, String name, String value) {
        StyleProp first, prev, prop;
        int cmp;

        prev = null;
        first = props;

        while (props != null) {
            cmp = props.name.compareTo(name);

            if (cmp == 0) {
                /* this property is already defined, ignore new value */
                return first;
            }

            if (cmp > 0) // props.name > name
            {
                /* insert before this */

                prop = new StyleProp(name, value, props);

                if (prev != null) {
                    prev.next = prop;
                } else {
                    first = prop;
                }

                return first;
            }

            prev = props;
            props = props.next;
        }

        prop = new StyleProp(name, value);

        if (prev != null) {
            prev.next = prop;
        } else {
            first = prop;
        }

        return first;
    }

    /*
     * Create sorted linked list of properties from style string It temporarily
     * places nulls in place of ':' and ';' to delimit the strings for the
     * property name and value. Some systems don't allow you to null literal
     * strings, so to avoid this, a copy is made first.
     */
    private StyleProp createProps(StyleProp prop, String style) {
        int name_end;
        int value_end;
        int value_start = 0;
        int name_start = 0;
        boolean more;

        name_start = 0;
        while (name_start < style.length()) {
            while (name_start < style.length() && style.charAt(name_start) == ' ') {
                ++name_start;
            }

            name_end = name_start;

            while (name_end < style.length()) {
                if (style.charAt(name_end) == ':') {
                    value_start = name_end + 1;
                    break;
                }

                ++name_end;
            }

            if (name_end >= style.length() || style.charAt(name_end) != ':') {
                break;
            }

            while (value_start < style.length() && style.charAt(value_start) == ' ') {
                ++value_start;
            }

            value_end = value_start;
            more = false;

            while (value_end < style.length()) {
                if (style.charAt(value_end) == ';') {
                    more = true;
                    break;
                }

                ++value_end;
            }

            prop = insertProperty(prop, style.substring(name_start, name_end), style.substring(value_start, value_end));

            if (more) {
                name_start = value_end + 1;
                continue;
            }

            break;
        }

        return prop;
    }

    private String createPropString(StyleProp props) {
        String style = "";
        int len;
        StyleProp prop;

        /* compute length */

        for (len = 0, prop = props; prop != null; prop = prop.next) {
            len += prop.name.length() + 2;
            len += prop.value.length() + 2;
        }

        for (prop = props; prop != null; prop = prop.next) {
            style = style.concat(prop.name);
            style = style.concat(": ");

            style = style.concat(prop.value);

            if (prop.next == null) {
                break;
            }

            style = style.concat("; ");
        }

        return style;
    }

    /*
     * create string with merged properties
     */
    private String addProperty(String style, String property) {
        StyleProp prop;

        prop = createProps(null, style);
        prop = createProps(prop, property);
        style = createPropString(prop);
        return style;
    }

    private String gensymClass(String tag) {
        String str;

        str = "c" + classNum;
        classNum++;
        return str;
    }

    private String findStyle(Lexer lexer, String tag, String properties) {
        Style style;

        for (style = lexer.styles; style != null; style = style.next) {
            if (style.tag.equals(tag) && style.properties.equals(properties)) {
                return style.tagClass;
            }
        }

        style = new Style(tag, gensymClass(tag), properties, lexer.styles);
        lexer.styles = style;
        return style.tagClass;
    }

    /*
     * Find style attribute in node, and replace it by corresponding class
     * attribute. Search for class in style dictionary otherwise gensym new
     * class and add to dictionary. Assumes that node doesn't have a class
     * attribute
     */
    private void style2Rule(Lexer lexer, Node node) {
        AttVal styleattr, classattr;
        String classname;

        styleattr = node.getAttrByName("style");

        if (styleattr != null) {
            classname = findStyle(lexer, node.element, styleattr.value);
            classattr = node.getAttrByName("class");

            /*
             * if there already is a class attribute then append class name
             * after a space
             */
            if (classattr != null) {
                classattr.value = classattr.value + " " + classname;
                node.removeAttribute(styleattr);
            } else /* reuse style attribute for class attribute */ {
                styleattr.attribute = "class";
                styleattr.value = classname;
            }
        }
    }

    private void addColorRule(Lexer lexer, String selector, String color) {
        if (color != null) {
            lexer.addStringLiteral(selector);
            lexer.addStringLiteral(" { color: ");
            lexer.addStringLiteral(color);
            lexer.addStringLiteral(" }\n");
        }
    }

    /*
     * move presentation attribs from body to style element background="foo" ->
     * body { background-image: url(foo) } bgcolor="foo" -> body {
     * background-color: foo } text="foo" -> body { color: foo } link="foo" ->
     * :link { color: foo } vlink="foo" -> :visited { color: foo } alink="foo"
     * -> :active { color: foo }
     */
    private void cleanBodyAttrs(Lexer lexer, Node body) {
        AttVal attr;
        String bgurl = null;
        String bgcolor = null;
        String color = null;

        attr = body.getAttrByName("background");

        if (attr != null) {
            bgurl = attr.value;
            attr.value = null;
            body.removeAttribute(attr);
        }

        attr = body.getAttrByName("bgcolor");

        if (attr != null) {
            bgcolor = attr.value;
            attr.value = null;
            body.removeAttribute(attr);
        }

        attr = body.getAttrByName("text");

        if (attr != null) {
            color = attr.value;
            attr.value = null;
            body.removeAttribute(attr);
        }

        if (bgurl != null || bgcolor != null || color != null) {
            lexer.addStringLiteral(" body {\n");

            if (bgurl != null) {
                lexer.addStringLiteral("  background-image: url(");
                lexer.addStringLiteral(bgurl);
                lexer.addStringLiteral(");\n");
            }

            if (bgcolor != null) {
                lexer.addStringLiteral("  background-color: ");
                lexer.addStringLiteral(bgcolor);
                lexer.addStringLiteral(";\n");
            }

            if (color != null) {
                lexer.addStringLiteral("  color: ");
                lexer.addStringLiteral(color);
                lexer.addStringLiteral(";\n");
            }

            lexer.addStringLiteral(" }\n");
        }

        attr = body.getAttrByName("link");

        if (attr != null) {
            addColorRule(lexer, " :link", attr.value);
            body.removeAttribute(attr);
        }

        attr = body.getAttrByName("vlink");

        if (attr != null) {
            addColorRule(lexer, " :visited", attr.value);
            body.removeAttribute(attr);
        }

        attr = body.getAttrByName("alink");

        if (attr != null) {
            addColorRule(lexer, " :active", attr.value);
            body.removeAttribute(attr);
        }
    }

    private boolean niceBody(Lexer lexer, Node doc) {
        Node body = doc.findBody(lexer.configuration.tt);

        if (body != null) {
            if (body.getAttrByName("background") != null || body.getAttrByName("bgcolor") != null
                || body.getAttrByName("text") != null || body.getAttrByName("link") != null
                || body.getAttrByName("vlink") != null || body.getAttrByName("alink") != null) {
                lexer.badLayout |= Report.USING_BODY;
                return false;
            }
        }

        return true;
    }

    /* create style element using rules from dictionary */
    private void createStyleElement(Lexer lexer, Node doc) {
        Node node, head, body;
        Style style;
        AttVal av;

        if (lexer.styles == null && niceBody(lexer, doc)) {
            return;
        }

        node = lexer.newNode(Node.StartTag, null, 0, 0, "style");
        node.implicit = true;

        /* insert type attribute */
        av = new AttVal(null, null, '"', "type", "text/css");
        av.dict = AttributeTable.getDefaultAttributeTable().findAttribute(av);
        node.attributes = av;

        body = doc.findBody(lexer.configuration.tt);

        lexer.txtstart = lexer.lexsize;

        if (body != null) {
            cleanBodyAttrs(lexer, body);
        }

        for (style = lexer.styles; style != null; style = style.next) {
            lexer.addCharToLexer(' ');
            lexer.addStringLiteral(style.tag);
            lexer.addCharToLexer('.');
            lexer.addStringLiteral(style.tagClass);
            lexer.addCharToLexer(' ');
            lexer.addCharToLexer('{');
            lexer.addStringLiteral(style.properties);
            lexer.addCharToLexer('}');
            lexer.addCharToLexer('\n');
        }

        lexer.txtend = lexer.lexsize;

        Node.insertNodeAtEnd(node, lexer.newNode(Node.TextNode, lexer.lexbuf, lexer.txtstart, lexer.txtend));

        /*
         * now insert style element into document head doc is root node. search
         * its children for html node the head node should be first child of
         * html node
         */

        head = doc.findHEAD(lexer.configuration.tt);

        if (head != null) {
            Node.insertNodeAtEnd(head, node);
        }
    }

    /* ensure bidirectional links are consistent */
    private void fixNodeLinks(Node node) {
        Node child;

        if (node.prev != null) {
            node.prev.next = node;
        } else {
            node.parent.content = node;
        }

        if (node.next != null) {
            node.next.prev = node;
        } else {
            node.parent.last = node;
        }

        for (child = node.content; child != null; child = child.next) {
            child.parent = node;
        }
    }

    /*
     * used to strip child of node when the node has one and only one child
     */
    private void stripOnlyChild(Node node) {
        Node child;

        child = node.content;
        node.content = child.content;
        node.last = child.last;
        child.content = null;

        for (child = node.content; child != null; child = child.next) {
            child.parent = node;
        }
    }

    /* used to strip font start and end tags */
    private void discardContainer(Node element, MutableObject pnode) {
        Node node;
        Node parent = element.parent;

        if (element.content != null) {
            element.last.next = element.next;

            if (element.next != null) {
                element.next.prev = element.last;
                element.last.next = element.next;
            } else {
                parent.last = element.last;
            }

            if (element.prev != null) {
                element.content.prev = element.prev;
                element.prev.next = element.content;
            } else {
                parent.content = element.content;
            }

            for (node = element.content; node != null; node = node.next) {
                node.parent = parent;
            }

            pnode.setObject(element.content);
        } else {
            if (element.next != null) {
                element.next.prev = element.prev;
            } else {
                parent.last = element.prev;
            }

            if (element.prev != null) {
                element.prev.next = element.next;
            } else {
                parent.content = element.next;
            }

            pnode.setObject(element.next);
        }

        element.next = null;
        element.content = null;
    }

    /*
     * Add style property to element, creating style attribute as needed and
     * adding ; delimiter
     */
    private void addStyleProperty(Node node, String property) {
        AttVal av;

        for (av = node.attributes; av != null; av = av.next) {
            if (av.attribute.equals("style")) {
                break;
            }
        }

        /* if style attribute already exists then insert property */

        if (av != null) {
            String s;

            s = addProperty(av.value, property);
            av.value = s;
        } else /* else create new style attribute */ {
            av = new AttVal(node.attributes, null, '"', "style", property);
            av.dict = AttributeTable.getDefaultAttributeTable().findAttribute(av);
            node.attributes = av;
        }
    }

    /*
     * Create new string that consists of the combined style properties in s1
     * and s2 To merge property lists, we build a linked list of property/values
     * and insert properties into the list in order, merging values for the same
     * property name.
     */
    private String mergeProperties(String s1, String s2) {
        String s;
        StyleProp prop;

        prop = createProps(null, s1);
        prop = createProps(prop, s2);
        s = createPropString(prop);
        return s;
    }

    private void mergeStyles(Node node, Node child) {
        AttVal av;
        String s1, s2, style;

        for (s2 = null, av = child.attributes; av != null; av = av.next) {
            if (av.attribute.equals("style")) {
                s2 = av.value;
                break;
            }
        }

        for (s1 = null, av = node.attributes; av != null; av = av.next) {
            if (av.attribute.equals("style")) {
                s1 = av.value;
                break;
            }
        }

        if (s1 != null) {
            if (s2 != null) /* merge styles from both */ {
                style = mergeProperties(s1, s2);
                av.value = style;
            }
        } else if (s2 != null) /* copy style of child */ {
            av = new AttVal(node.attributes, null, '"', "style", s2);
            av.dict = AttributeTable.getDefaultAttributeTable().findAttribute(av);
            node.attributes = av;
        }
    }

    private String fontSize2Name(String size) {
        /*
         * String[] sizes = { "50%", "60%", "80%", null, "120%", "150%", "200%"
         * };
         */

        String[] sizes = { "60%", "70%", "80%", null, "120%", "150%", "200%" };
        String buf;

        if (size.length() > 0 && '0' <= size.charAt(0) && size.charAt(0) <= '6') {
            int n = size.charAt(0) - '0';
            return sizes[n];
        }

        if (size.length() > 0 && size.charAt(0) == '-') {
            if (size.length() > 1 && '0' <= size.charAt(1) && size.charAt(1) <= '6') {
                int n = size.charAt(1) - '0';
                double x;

                for (x = 1.0; n > 0; --n) {
                    x *= 0.8;
                }

                x *= 100.0;
                buf = "" + (int) x + "%";

                return buf;
            }

            return "smaller"; /* "70%"; */
        }

        if (size.length() > 1 && '0' <= size.charAt(1) && size.charAt(1) <= '6') {
            int n = size.charAt(1) - '0';
            double x;

            for (x = 1.0; n > 0; --n) {
                x *= 1.2;
            }

            x *= 100.0;
            buf = "" + (int) x + "%";

            return buf;
        }

        return "larger"; /* "140%" */
    }

    private void addFontFace(Node node, String face) {
        addStyleProperty(node, "font-family: " + face);
    }

    private void addFontSize(Node node, String size) {
        String value;

        if (size.equals("6") && node.tag == tt.tagP) {
            node.element = "h1";
            tt.findTag(node);
            return;
        }

        if (size.equals("5") && node.tag == tt.tagP) {
            node.element = "h2";
            tt.findTag(node);
            return;
        }

        if (size.equals("4") && node.tag == tt.tagP) {
            node.element = "h3";
            tt.findTag(node);
            return;
        }

        value = fontSize2Name(size);

        if (value != null) {
            addStyleProperty(node, "font-size: " + value);
        }
    }

    private void addFontColor(Node node, String color) {
        addStyleProperty(node, "color: " + color);
    }

    private void addAlign(Node node, String align) {
        /* force alignment value to lower case */
        addStyleProperty(node, "text-align: " + align.toLowerCase());
    }

    /*
     * add style properties to node corresponding to the font face, size and
     * color attributes
     */
    private void addFontStyles(Node node, AttVal av) {
        while (av != null) {
            if (av.attribute.equals("face")) {
                addFontFace(node, av.value);
            } else if (av.attribute.equals("size")) {
                addFontSize(node, av.value);
            } else if (av.attribute.equals("color")) {
                addFontColor(node, av.value);
            }

            av = av.next;
        }
    }

    /*
     * Symptom: <p align=center> Action: <p style="text-align: center">
     */
    private void textAlign(Lexer lexer, Node node) {
        AttVal av, prev;

        prev = null;

        for (av = node.attributes; av != null; av = av.next) {
            if (av.attribute.equals("align")) {
                if (prev != null) {
                    prev.next = av.next;
                } else {
                    node.attributes = av.next;
                }

                if (av.value != null) {
                    addAlign(node, av.value);
                }

                break;
            }

            prev = av;
        }
    }

    /*
     * The clean up rules use the pnode argument to return the next node when
     * the orignal node has been deleted
     */

    /*
     * Symptom: <dir> <li> where <li> is only child Action: coerce <dir> <li> to
     * <div> with indent.
     */

    private boolean dir2Div(Lexer lexer, Node node, MutableObject pnode) {
        Node child;

        if (node.tag == tt.tagDir || node.tag == tt.tagUl || node.tag == tt.tagOl) {
            child = node.content;

            if (child == null) {
                return false;
            }

            /* check child has no peers */

            if (child.next != null) {
                return false;
            }

            if (child.tag != tt.tagLi) {
                return false;
            }

            if (!child.implicit) {
                return false;
            }

            /* coerce dir to div */

            node.tag = tt.tagDiv;
            node.element = "div";
            addStyleProperty(node, "margin-left: 2em");
            stripOnlyChild(node);
            return true;

            //#if 0
            //Node content;
            //Node last;
            //content = child.content;
            //last = child.last;
            //child.content = null;

            /* adjust parent and set margin on contents of <li> */

            //for (child = content; child != null; child = child.next)
            //{
            //    child.parent = node.parent;
            //    addStyleProperty(child, "margin-left: 1em");
            //}
            /* hook first/last into sequence */

            //if (content != null)
            //{
            //    content.prev = node.prev;
            //    last.next = node.next;
            //    fixNodeLinks(content);
            //    fixNodeLinks(last);
            //}
            //node.next = null;
            /* ensure that new node is cleaned */
            //pnode.setObject(cleanNode(lexer, content));
            //return true;
            //#endif
        }

        return false;
    }

    /*
     * Symptom: <center> Action: replace <center> by <div
     * style="text-align: center">
     */

    private boolean center2Div(Lexer lexer, Node node, MutableObject pnode) {
        if (node.tag == tt.tagCenter) {
            if (lexer.configuration.DropFontTags) {
                if (node.content != null) {
                    Node last = node.last;
                    Node parent = node.parent;

                    discardContainer(node, pnode);

                    node = lexer.inferredTag("br");

                    if (last.next != null) {
                        last.next.prev = node;
                    }

                    node.next = last.next;
                    last.next = node;
                    node.prev = last;

                    if (parent.last == last) {
                        parent.last = node;
                    }

                    node.parent = parent;
                } else {
                    Node prev = node.prev;
                    Node next = node.next;
                    Node parent = node.parent;
                    discardContainer(node, pnode);

                    node = lexer.inferredTag("br");
                    node.next = next;
                    node.prev = prev;
                    node.parent = parent;

                    if (next != null) {
                        next.prev = node;
                    } else {
                        parent.last = node;
                    }

                    if (prev != null) {
                        prev.next = node;
                    } else {
                        parent.content = node;
                    }
                }

                return true;
            }
            node.tag = tt.tagDiv;
            node.element = "div";
            addStyleProperty(node, "text-align: center");
            return true;
        }

        return false;
    }

    /*
     * Symptom <div><div>...</div></div> Action: merge the two divs This is
     * useful after nested <dir>s used by Word for indenting have been converted
     * to <div>s
     */
    private boolean mergeDivs(Lexer lexer, Node node, MutableObject pnode) {
        Node child;

        if (node.tag != tt.tagDiv) {
            return false;
        }

        child = node.content;

        if (child == null) {
            return false;
        }

        if (child.tag != tt.tagDiv) {
            return false;
        }

        if (child.next != null) {
            return false;
        }

        mergeStyles(node, child);
        stripOnlyChild(node);
        return true;
    }

    /*
     * Symptom: <ul><li><ul>...</ul></li></ul> Action: discard outer list
     */

    private boolean nestedList(Lexer lexer, Node node, MutableObject pnode) {
        Node child, list;

        if (node.tag == tt.tagUl || node.tag == tt.tagOl) {
            child = node.content;

            if (child == null) {
                return false;
            }

            /* check child has no peers */

            if (child.next != null) {
                return false;
            }

            list = child.content;

            if (list == null) {
                return false;
            }

            if (list.tag != node.tag) {
                return false;
            }

            pnode.setObject(node.next);

            /* move inner list node into position of outer node */
            list.prev = node.prev;
            list.next = node.next;
            list.parent = node.parent;
            fixNodeLinks(list);

            /* get rid of outer ul and its li */
            child.content = null;
            node.content = null;
            node.next = null;

            /*
             * If prev node was a list the chances are this node should be
             * appended to that list. Word has no way of recognizing nested
             * lists and just uses indents
             */

            if (list.prev != null) {
                node = list;
                list = node.prev;

                if (list.tag == tt.tagUl || list.tag == tt.tagOl) {
                    list.next = node.next;

                    if (list.next != null) {
                        list.next.prev = list;
                    }

                    child = list.last; /* <li> */

                    node.parent = child;
                    node.next = null;
                    node.prev = child.last;
                    fixNodeLinks(node);
                }
            }

            cleanNode(lexer, node);
            return true;
        }

        return false;
    }

    /*
     * Symptom: the only child of a block-level element is a presentation
     * element such as B, I or FONT Action: add style "font-weight: bold" to the
     * block and strip the <b> element, leaving its children. example: <p>
     * <b><font face="Arial" size="6">Draft Recommended Practice</font></b> </p>
     * becomes: <p style="font-weight: bold; font-family: Arial; font-size: 6">
     * Draft Recommended Practice </p> This code also replaces the align
     * attribute by a style attribute. However, to avoid CSS problems with
     * Navigator 4, this isn't done for the elements: caption, tr and table
     */
    private boolean blockStyle(Lexer lexer, Node node, MutableObject pnode) {
        Node child;

        if ((node.tag.model & (Dict.CM_BLOCK | Dict.CM_LIST | Dict.CM_DEFLIST | Dict.CM_TABLE)) != 0) {
            if (node.tag != tt.tagTable && node.tag != tt.tagTr && node.tag != tt.tagLi) {
                /* check for align attribute */
                if (node.tag != tt.tagCaption) {
                    textAlign(lexer, node);
                }

                child = node.content;

                if (child == null) {
                    return false;
                }

                /* check child has no peers */

                if (child.next != null) {
                    return false;
                }

                if (child.tag == tt.tagB) {
                    mergeStyles(node, child);
                    addStyleProperty(node, "font-weight: bold");
                    stripOnlyChild(node);
                    return true;
                }

                if (child.tag == tt.tagI) {
                    mergeStyles(node, child);
                    addStyleProperty(node, "font-style: italic");
                    stripOnlyChild(node);
                    return true;
                }

                if (child.tag == tt.tagFont) {
                    mergeStyles(node, child);
                    addFontStyles(node, child.attributes);
                    stripOnlyChild(node);
                    return true;
                }
            }
        }

        return false;
    }

    /* the only child of table cell or an inline element such as em */
    private boolean inlineStyle(Lexer lexer, Node node, MutableObject pnode) {
        Node child;

        if (node.tag != tt.tagFont && (node.tag.model & (Dict.CM_INLINE | Dict.CM_ROW)) != 0) {
            child = node.content;

            if (child == null) {
                return false;
            }

            /* check child has no peers */

            if (child.next != null) {
                return false;
            }

            if (child.tag == tt.tagB && lexer.configuration.LogicalEmphasis) {
                mergeStyles(node, child);
                addStyleProperty(node, "font-weight: bold");
                stripOnlyChild(node);
                return true;
            }

            if (child.tag == tt.tagI && lexer.configuration.LogicalEmphasis) {
                mergeStyles(node, child);
                addStyleProperty(node, "font-style: italic");
                stripOnlyChild(node);
                return true;
            }

            if (child.tag == tt.tagFont) {
                mergeStyles(node, child);
                addFontStyles(node, child.attributes);
                stripOnlyChild(node);
                return true;
            }
        }

        return false;
    }

    /*
     * Replace font elements by span elements, deleting the font element's
     * attributes and replacing them by a single style attribute.
     */
    private boolean font2Span(Lexer lexer, Node node, MutableObject pnode) {
        AttVal av, style, next;

        if (node.tag == tt.tagFont) {
            if (lexer.configuration.DropFontTags) {
                discardContainer(node, pnode);
                return false;
            }

            /* if FONT is only child of parent element then leave alone */
            if (node.parent.content == node && node.next == null) {
                return false;
            }

            addFontStyles(node, node.attributes);

            /* extract style attribute and free the rest */
            av = node.attributes;
            style = null;

            while (av != null) {
                next = av.next;

                if (av.attribute.equals("style")) {
                    av.next = null;
                    style = av;
                }

                av = next;
            }

            node.attributes = style;

            node.tag = tt.tagSpan;
            node.element = "span";

            return true;
        }

        return false;
    }

    /*
     * Applies all matching rules to a node.
     */
    private Node cleanNode(Lexer lexer, Node node) {
        Node next = null;
        MutableObject o = new MutableObject();
        boolean b = false;

        for (next = node; node.isElement(); node = next) {
            o.setObject(next);

            b = dir2Div(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = nestedList(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = center2Div(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = mergeDivs(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = blockStyle(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = inlineStyle(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            b = font2Span(lexer, node, o);
            next = (Node) o.getObject();
            if (b) {
                continue;
            }

            break;
        }

        return next;
    }

    private Node createStyleProperties(Lexer lexer, Node node) {
        Node child;

        if (node.content != null) {
            for (child = node.content; child != null; child = child.next) {
                child = createStyleProperties(lexer, child);
            }
        }

        return cleanNode(lexer, node);
    }

    private void defineStyleRules(Lexer lexer, Node node) {
        Node child;

        if (node.content != null) {
            for (child = node.content; child != null; child = child.next) {
                defineStyleRules(lexer, child);
            }
        }

        style2Rule(lexer, node);
    }

    public void cleanTree(Lexer lexer, Node doc) {
        doc = createStyleProperties(lexer, doc);

        if (!lexer.configuration.MakeClean) {
            defineStyleRules(lexer, doc);
            createStyleElement(lexer, doc);
        }
    }

    /* simplifies <b><b> ... </b> ...</b> etc. */
    public void nestedEmphasis(Node node) {
        MutableObject o = new MutableObject();
        Node next;

        while (node != null) {
            next = node.next;

            if ((node.tag == tt.tagB || node.tag == tt.tagI) && node.parent != null && node.parent.tag == node.tag) {
                /* strip redundant inner element */
                o.setObject(next);
                discardContainer(node, o);
                next = (Node) o.getObject();
                node = next;
                continue;
            }

            if (node.content != null) {
                nestedEmphasis(node.content);
            }

            node = next;
        }
    }

    /* replace i by em and b by strong */
    public void emFromI(Node node) {
        while (node != null) {
            if (node.tag == tt.tagI) {
                node.element = tt.tagEm.name;
                node.tag = tt.tagEm;
            } else if (node.tag == tt.tagB) {
                node.element = tt.tagStrong.name;
                node.tag = tt.tagStrong;
            }

            if (node.content != null) {
                emFromI(node.content);
            }

            node = node.next;
        }
    }

    /*
     * Some people use dir or ul without an li to indent the content. The
     * pattern to look for is a list with a single implicit li. This is
     * recursively replaced by an implicit blockquote.
     */
    public void list2BQ(Node node) {
        while (node != null) {
            if (node.content != null) {
                list2BQ(node.content);
            }

            if (node.tag != null && node.tag.parser == ParserImpl.getParseList() && node.hasOneChild()
                && node.content.implicit) {
                stripOnlyChild(node);
                node.element = tt.tagBlockquote.name;
                node.tag = tt.tagBlockquote;
                node.implicit = true;
            }

            node = node.next;
        }
    }

    /*
     * Replace implicit blockquote by div with an indent taking care to reduce
     * nested blockquotes to a single div with the indent set to match the
     * nesting depth
     */
    public void bQ2Div(Node node) {
        int indent;
        String indent_buf;

        while (node != null) {
            if (node.tag == tt.tagBlockquote && node.implicit) {
                indent = 1;

                while (node.hasOneChild() && node.content.tag == tt.tagBlockquote && node.implicit) {
                    ++indent;
                    stripOnlyChild(node);
                }

                if (node.content != null) {
                    bQ2Div(node.content);
                }

                indent_buf = "margin-left: " + new Integer(2 * indent).toString() + "em";

                node.element = tt.tagDiv.name;
                node.tag = tt.tagDiv;
                node.addAttribute("style", indent_buf);
            } else if (node.content != null) {
                bQ2Div(node.content);
            }

            node = node.next;
        }
    }

    /* node is <![if ...]> prune up to <![endif]> */
    public Node pruneSection(Lexer lexer, Node node) {
        for (; ; ) {
            /* discard node and returns next */
            node = Node.discardElement(node);

            if (node == null) {
                return null;
            }

            if (node.type == Node.SectionTag) {
                if (Lexer.getString(node.textarray, node.start, 2).equals("if")) {
                    node = pruneSection(lexer, node);
                    continue;
                }

                if (Lexer.getString(node.textarray, node.start, 5).equals("endif")) {
                    node = Node.discardElement(node);
                    break;
                }
            }
        }

        return node;
    }

    public void dropSections(Lexer lexer, Node node) {
        while (node != null) {
            if (node.type == Node.SectionTag) {
                /* prune up to matching endif */
                if (Lexer.getString(node.textarray, node.start, 2).equals("if")) {
                    node = pruneSection(lexer, node);
                    continue;
                }

                /* discard others as well */
                node = Node.discardElement(node);
                continue;
            }

            if (node.content != null) {
                dropSections(lexer, node.content);
            }

            node = node.next;
        }
    }

    public void purgeAttributes(Node node) {
        AttVal attr = node.attributes;
        AttVal next = null;
        AttVal prev = null;

        while (attr != null) {
            next = attr.next;

            /* special check for class="Code" denoting pre text */
            if (attr.attribute != null && attr.value != null && attr.attribute.equals("class")
                && attr.value.equals("Code")) {
                prev = attr;
            } else if (attr.attribute != null
                       && (attr.attribute.equals("class") || attr.attribute.equals("style")
                           || attr.attribute.equals("lang") || attr.attribute.startsWith("x:") || (attr.attribute
                                                                                                       .equals("height") || attr.attribute.equals("width"))
                                                                                                  && (node.tag == tt.tagTd || node.tag == tt.tagTr || node.tag == tt.tagTh))) {
                if (prev != null) {
                    prev.next = next;
                } else {
                    node.attributes = next;
                }
            } else {
                prev = attr;
            }

            attr = next;
        }
    }

    /* Word2000 uses span excessively, so we strip span out */
    public Node stripSpan(Lexer lexer, Node span) {
        Node node;
        Node prev = null;
        Node content;

        /*
         * deal with span elements that have content by splicing the content in
         * place of the span after having processed it
         */

        cleanWord2000(lexer, span.content);
        content = span.content;

        if (span.prev != null) {
            prev = span.prev;
        } else if (content != null) {
            node = content;
            content = content.next;
            Node.removeNode(node);
            Node.insertNodeBeforeElement(span, node);
            prev = node;
        }

        while (content != null) {
            node = content;
            content = content.next;
            Node.removeNode(node);
            Node.insertNodeAfterElement(prev, node);
            prev = node;
        }

        if (span.next == null) {
            span.parent.last = prev;
        }

        node = span.next;
        span.content = null;
        Node.discardElement(span);
        return node;
    }

    /* map non-breaking spaces to regular spaces */
    private void normalizeSpaces(Lexer lexer, Node node) {
        while (node != null) {
            if (node.content != null) {
                normalizeSpaces(lexer, node.content);
            }

            if (node.type == Node.TextNode) {
                int i;
                MutableInteger c = new MutableInteger();
                int p = node.start;

                for (i = node.start; i < node.end; ++i) {
                    c.value = node.textarray[i];

                    /* look for UTF-8 multibyte character */
                    if (c.value > 0x7F) {
                        i += PPrint.getUTF8(node.textarray, i, c);
                    }

                    if (c.value == 160) {
                        c.value = ' ';
                    }

                    p = PPrint.putUTF8(node.textarray, p, c.value);
                }
            }

            node = node.next;
        }
    }

    /*
     * This is a major clean up to strip out all the extra stuff you get when
     * you save as web page from Word 2000. It doesn't yet know what to do with
     * VML tags, but these will appear as errors unless you declare them as new
     * tags, such as o:p which needs to be declared as inline.
     */
    public void cleanWord2000(Lexer lexer, Node node) {
        /* used to a list from a sequence of bulletted p's */
        Node list = null;

        while (node != null) {
            /* discard Word's style verbiage */
            if (node.tag == tt.tagStyle || node.tag == tt.tagMeta || node.type == Node.CommentTag) {
                node = Node.discardElement(node);
                continue;
            }

            /* strip out all span tags Word scatters so liberally! */
            if (node.tag == tt.tagSpan) {
                node = stripSpan(lexer, node);
                continue;
            }

            /* get rid of Word's xmlns attributes */
            if (node.tag == tt.tagHtml) {
                /* check that it's a Word 2000 document */
                if (node.getAttrByName("xmlns:o") == null) {
                    return;
                }
            }

            if (node.tag == tt.tagLink) {
                AttVal attr = node.getAttrByName("rel");

                if (attr != null && attr.value != null && attr.value.equals("File-List")) {
                    node = Node.discardElement(node);
                    continue;
                }
            }

            /* discard empty paragraphs */
            if (node.content == null && node.tag == tt.tagP) {
                node = Node.discardElement(node);
                continue;
            }

            if (node.tag == tt.tagP) {
                AttVal attr = node.getAttrByName("class");

                /* map sequence of <p class="MsoListBullet"> to <ul>...</ul> */
                if (attr != null && attr.value != null && attr.value.equals("MsoListBullet")) {
                    Node.coerceNode(lexer, node, tt.tagLi);

                    if (list == null || list.tag != tt.tagUl) {
                        list = lexer.inferredTag("ul");
                        Node.insertNodeBeforeElement(node, list);
                    }

                    purgeAttributes(node);

                    if (node.content != null) {
                        cleanWord2000(lexer, node.content);
                    }

                    /* remove node and append to contents of list */
                    Node.removeNode(node);
                    Node.insertNodeAtEnd(list, node);
                    node = list.next;
                }
                /* map sequence of <p class="Code"> to <pre>...</pre> */
                else if (attr != null && attr.value != null && attr.value.equals("Code")) {
                    Node br = lexer.newLineNode();
                    normalizeSpaces(lexer, node);

                    if (list == null || list.tag != tt.tagPre) {
                        list = lexer.inferredTag("pre");
                        Node.insertNodeBeforeElement(node, list);
                    }

                    /* remove node and append to contents of list */
                    Node.removeNode(node);
                    Node.insertNodeAtEnd(list, node);
                    stripSpan(lexer, node);
                    Node.insertNodeAtEnd(list, br);
                    node = list.next;
                } else {
                    list = null;
                }
            } else {
                list = null;
            }

            /* strip out style and class attributes */
            if (node.type == Node.StartTag || node.type == Node.StartEndTag) {
                purgeAttributes(node);
            }

            if (node.content != null) {
                cleanWord2000(lexer, node.content);
            }

            node = node.next;
        }
    }

    public boolean isWord2000(Node root, TagTable tt) {
        Node html = root.findHTML(tt);

        return html != null && html.getAttrByName("xmlns:o") != null;
    }
}
